﻿// Copyright (c) 2011 OpenNETCF Consulting, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to 
// the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 

// DEVELOPER'S NOTE:
// Since I'm a fan of not reinventing the wheel, I suppressed the urge to write the image conversion
// routines needed for this app in unsafe code (which may have yielded better perf).  Since I like reuse,
// the 8bpp image conversion algorithms in this code were lifted wholesale from the CodeProject project 
// located at:
//    http://www.codeproject.com/KB/graphics/BMP_files_and_color_depth.aspx
// 
// Credit goes to the original developer, Statsenko Dima, for that workusing System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing.Imaging;
using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace OpenNETCF.SplashGenerator
{
    internal class ImageConverter
    {
        public static Bitmap ConvertTo8bppFormat(Bitmap bmpSource)
        {
            if (bmpSource.PixelFormat == PixelFormat.Format8bppIndexed) return (Bitmap)bmpSource.Clone();

            int imageWidth = bmpSource.Width;
            int imageHeight = bmpSource.Height;

            Bitmap bmpDest = null;
            BitmapData bmpDataDest = null;
            BitmapData bmpDataSource = null;

            try
            {
                // Create new image with 8BPP format
                bmpDest = new Bitmap(
                    imageWidth,
                    imageHeight,
                    PixelFormat.Format8bppIndexed
                    );

                // Lock bitmap in memory
                bmpDataDest = bmpDest.LockBits(
                    new Rectangle(0, 0, imageWidth, imageHeight),
                    ImageLockMode.ReadWrite,
                    bmpDest.PixelFormat
                    );

                bmpDataSource = bmpSource.LockBits(
                    new Rectangle(0, 0, imageWidth, imageHeight),
                    ImageLockMode.ReadOnly,
                    bmpSource.PixelFormat
                );

                int pixelSize = GetPixelInfoSize(bmpDataSource.PixelFormat);
                byte[] buffer = new byte[imageWidth * imageHeight * pixelSize];
                byte[] destBuffer = new byte[imageWidth * imageHeight];

                // Read all data to buffer
                ReadBmpData(bmpDataSource, buffer, pixelSize, imageWidth, imageHeight);

                // Get color indexes
                MatchColors(buffer, destBuffer, pixelSize, bmpDest.Palette);

                // Copy all colors to destination bitmaps
                WriteBmpData(bmpDataDest, destBuffer, imageWidth, imageHeight);

                return bmpDest;
            }
            finally
            {
                if (bmpDest != null) bmpDest.UnlockBits(bmpDataDest);
                if (bmpSource != null) bmpSource.UnlockBits(bmpDataSource);
            }
        }

        public static int GetPixelInfoSize(PixelFormat format)
        {
            switch (format)
            {
                case PixelFormat.Format8bppIndexed:
                    return 1;
                case PixelFormat.Format16bppArgb1555:
                case PixelFormat.Format16bppGrayScale:
                case PixelFormat.Format16bppRgb555:
                case PixelFormat.Format16bppRgb565:
                    return 2;
                case PixelFormat.Format24bppRgb:
                    return 3;
                case PixelFormat.Format32bppArgb:
                case PixelFormat.Format32bppPArgb:
                case PixelFormat.Format32bppRgb:
                    return 4;
                default:
                    throw new NotSupportedException("Source Image pixel format not supported");
            }
        }

        private static void ReadBmpData(
            BitmapData bmpDataSource,
            byte[] buffer,
            int pixelSize,
            int width,
            int height)
        {
            // Get unmanaged data start address
            int addrStart = bmpDataSource.Scan0.ToInt32();

            for (int i = 0; i < height; i++)
            {
                // Get address of next row
                IntPtr realByteAddr = new IntPtr(addrStart +
                    System.Convert.ToInt32(i * bmpDataSource.Stride)
                    );

                // Perform copy from unmanaged memory
                // to managed buffer
                Marshal.Copy(
                    realByteAddr,
                    buffer,
                    (int)(i * width * pixelSize),
                    (int)(width * pixelSize)
                );
            }
        }

        /// <summary>
        /// Writes bitmap data to unmanaged memory
        /// </summary>
        private static void WriteBmpData(
            BitmapData bmpDataDest,
            byte[] destBuffer,
            int imageWidth,
            int imageHeight)
        {
            // Get unmanaged data start address
            int addrStart = bmpDataDest.Scan0.ToInt32();

            for (int i = 0; i < imageHeight; i++)
            {
                // Get address of next row
                IntPtr realByteAddr = new IntPtr(addrStart +
                    System.Convert.ToInt32(i * bmpDataDest.Stride)
                    );

                // Perform copy from managed buffer
                // to unmanaged memory
                Marshal.Copy(
                    destBuffer,
                    i * imageWidth,
                    realByteAddr,
                    imageWidth
                );
            }
        }

        private static Dictionary<int, byte> m_knownColors = new Dictionary<int, byte>();
        private static void MatchColors(
            byte[] buffer,
            byte[] destBuffer,
            int pixelSize,
            ColorPalette pallete)
        {
            int length = destBuffer.Length;

            // Temp storage for color info
            byte[] temp = new byte[pixelSize];

            int palleteSize = pallete.Entries.Length;

            int mult_1 = 256;
            int mult_2 = 256 * 256;

            int currentKey = 0;

            // For each color
            for (int i = 0; i < length; i++)
            {
                // Get next color
                Array.Copy(buffer, i * pixelSize, temp, 0, pixelSize);

                // Build key for hash table
                currentKey = temp[0] + temp[1] * mult_1 + temp[2] * mult_2;

                // If hash table already contains such color - fetch it
                // Otherwise perform calculation of similar color and save it to HT
                if (!m_knownColors.ContainsKey(currentKey))
                {
                    destBuffer[i] = GetSimilarColor(pallete, temp, palleteSize);
                    m_knownColors.Add(currentKey, destBuffer[i]);
                }
                else
                {
                    destBuffer[i] = m_knownColors[currentKey];
                }
            }// for
        }

        /// <summary>
        /// Returns Similar color
        /// </summary>
        /// <param name="palette"></param>
        /// <param name="color"></param>
        /// <returns></returns>
        private static byte GetSimilarColor(ColorPalette palette, byte[] color, int palleteSize)
        {
            byte minDiff = byte.MaxValue;
            byte index = 0;

            if (color.Length == 3)// Implemented for 24bpp color
            {
                // Loop all pallete ( 256 colors )
                for (int i = 0; i < palleteSize - 1; i++)
                {
                    // Calculate similar color
                    byte currentDiff = GetMaxDiff(color, palette.Entries[i]);

                    if (currentDiff < minDiff)
                    {
                        minDiff = currentDiff;
                        index = (byte)i;
                    }
                }// for
            }
            else// TODO implement it for other color types
            {
                throw new ApplicationException("Only 24bit colors supported now");
            }

            return index;
        }

        /// <summary>
        /// Return similar color
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private static byte GetMaxDiff(byte[] a, Color b)
        {
            // Get difference between components ( red green blue )
            // of given color and appropriate components of pallete color
            byte bDiff = a[0] > b.B ? (byte)(a[0] - b.B) : (byte)(b.B - a[0]);
            byte gDiff = a[1] > b.G ? (byte)(a[1] - b.G) : (byte)(b.G - a[1]);
            byte rDiff = a[2] > b.R ? (byte)(a[2] - b.R) : (byte)(b.R - a[2]);

            // Get max difference
            byte max = bDiff > gDiff ? bDiff : gDiff;
            max = max > rDiff ? max : rDiff;

            return max;
        }
    }
}
